Skip to content

카드 컴포넌트 개발#47

Merged
yooolleee merged 4 commits into
mainfrom
feature/card
Feb 10, 2026
Merged

카드 컴포넌트 개발#47
yooolleee merged 4 commits into
mainfrom
feature/card

Conversation

@yooolleee

Copy link
Copy Markdown
Contributor

Summary

  • 이 PR에서 변경한 내용 한 줄 요약

이미지 업로드 컴포넌트도 figma 시안상 Card 영역에 있어서 card 폴더안에 넣어두었습니다.

Issue

  • Closes #

Scope

  • 포함

    • 변경한 내용
  • 특이사항

@yooolleee yooolleee added this to the 컴포넌트 milestone Feb 10, 2026
@yooolleee yooolleee requested review from a team, HWAN0218, Jieunsse and jungwon123 February 10, 2026 03:45
@yooolleee yooolleee self-assigned this Feb 10, 2026
@yooolleee yooolleee added the 기능추가 기능개발 및 추가할 때 사용해주세요. label Feb 10, 2026
@github-project-automation github-project-automation Bot moved this to 개발 대기중 in workers Feb 10, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @yooolleee, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 다양한 카드 기반 UI 컴포넌트들을 개발하여 사용자 인터페이스의 재사용성과 일관성을 높이는 데 중점을 둡니다. 게시글, 이미지 업로드, 할 일 목록 및 상세 정보 등 여러 기능에 필요한 카드 컴포넌트들을 모듈화하여 구현함으로써, 향후 개발 효율성을 증대시키고 유지보수를 용이하게 합니다. 또한, 개발 환경 설정을 업데이트하여 코드 품질 관리를 강화했습니다.

Highlights

  • 새로운 ArticleCard 컴포넌트 추가: 게시글 정보를 카드 형태로 표시하는 ArticleCard 컴포넌트와 관련 스타일 및 Storybook 스토리가 추가되었습니다.
  • ImageUpload 컴포넌트 개발: 이미지 업로드 기능을 제공하는 ImageUpload 컴포넌트와 관련 스타일 및 Storybook 스토리가 새로 개발되었습니다.
  • TaskCard 컴포넌트 추가: 할 일 목록을 간결하게 표시하는 TaskCard 컴포넌트와 관련 스타일 및 Storybook 스토리가 추가되었습니다.
  • TaskDetailCard 컴포넌트 구현: 할 일의 상세 정보를 표시하고 댓글 및 액션 기능을 포함하는 TaskDetailCard 컴포넌트와 관련 스타일 및 Storybook 스토리가 구현되었습니다.
  • KebabMenu 컴포넌트 추가: 수정 및 삭제와 같은 컨텍스트 메뉴를 제공하는 KebabMenu 컴포넌트와 관련 스타일이 추가되었습니다.
  • VS Code 설정 업데이트: VS Code 설정 파일에 CSS, SCSS, LESS 린터 관련 규칙이 추가되어 벤더 프리픽스 경고를 무시하도록 설정되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .vscode/settings.json
    • CSS 린터 설정을 추가했습니다.
  • src/components/Card/ArticleCard/ArticleCard.module.css
    • ArticleCard 컴포넌트의 스타일을 정의했습니다.
  • src/components/Card/ArticleCard/ArticleCard.stories.tsx
    • ArticleCard 컴포넌트의 Storybook 스토리를 추가했습니다.
  • src/components/Card/ArticleCard/ArticleCard.tsx
    • ArticleCard 컴포넌트의 로직을 구현했습니다.
  • src/components/Card/ImageUpload/ImageUpload.module.css
    • ImageUpload 컴포넌트의 스타일을 정의했습니다.
  • src/components/Card/ImageUpload/ImageUpload.stories.tsx
    • ImageUpload 컴포넌트의 Storybook 스토리를 추가했습니다.
  • src/components/Card/ImageUpload/ImageUpload.tsx
    • ImageUpload 컴포넌트의 로직을 구현했습니다.
  • src/components/Card/TaskCard/TaskCard.module.css
    • TaskCard 컴포넌트의 스타일을 정의했습니다.
  • src/components/Card/TaskCard/TaskCard.stories.tsx
    • TaskCard 컴포넌트의 Storybook 스토리를 추가했습니다.
  • src/components/Card/TaskCard/TaskCard.tsx
    • TaskCard 컴포넌트의 로직을 구현했습니다.
  • src/components/Card/TaskDetailCard/TaskDetailCard.module.css
    • TaskDetailCard 컴포넌트의 스타일을 정의했습니다.
  • src/components/Card/TaskDetailCard/TaskDetailCard.stories.tsx
    • TaskDetailCard 컴포넌트의 Storybook 스토리를 추가했습니다.
  • src/components/Card/TaskDetailCard/TaskDetailCard.tsx
    • TaskDetailCard 컴포넌트의 로직을 구현했습니다.
  • src/components/KebabMenu/KebabMenu.module.css
    • KebabMenu 컴포넌트의 스타일을 정의했습니다.
  • src/components/KebabMenu/KebabMenu.tsx
    • KebabMenu 컴포넌트의 로직을 구현했습니다.
Activity
  • 현재까지 특별한 활동은 없습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new UI components: ArticleCard, ImageUpload, TaskCard, TaskDetailCard, and KebabMenu, complete with their respective CSS modules and Storybook stories. The ArticleCard displays article details with responsive styling, including logic for formatting dates and like counts. The ImageUpload component allows users to upload multiple images, showing previews and providing client-side validation for file size and type. The TaskCard is a simple card for displaying task labels and counts, while the TaskDetailCard offers a comprehensive view of a task, including assignee, dates, description, completion status, comments, and actions via an integrated KebabMenu. The KebabMenu provides a reusable dropdown for edit and delete functionalities. Additionally, the .vscode/settings.json file was updated to ignore vendor prefixes in CSS linting. Review comments suggest improving accessibility by using next/image/Image with meaningful alt text for article images and uploaded image previews, enhancing user experience by replacing alert() with consistent UI for validation errors in ImageUpload, and improving code maintainability by extracting pure utility functions (formatDate, formatLikeCount) and API interfaces (Writer, Comment) out of components into shared utility and type files. Other suggestions include defining hardcoded values as constants and removing unused CSS classes and component props.

<div className={styles.rightSection}>
{image && (
<div className={styles.imageWrapper}>
<img src={image} alt="" className={styles.image} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

게시글 이미지에 일반 <img> 태그 대신 next/imageImage 컴포넌트를 사용하면 이미지 최적화, 지연 로딩 등의 이점을 얻을 수 있습니다. 또한 alt 속성에는 빈 문자열 대신 게시글 제목 등 의미 있는 대체 텍스트를 제공하여 웹 접근성을 향상시키는 것이 좋습니다. fill 속성을 사용하려면 부모 요소인 .imageWrapperposition: relative; 스타일을 추가해야 합니다.

Suggested change
<img src={image} alt="" className={styles.image} />
<Image src={image} alt={title} fill sizes="(max-width: 767px) 80px, (max-width: 1199px) 100px, 120px" style={{ objectFit: 'cover' }} />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 왜 300바이트 SVG에 next/image를 쓰는 게 "오히려 손해"일 수 있나?
    사용자님 생각이 맞는 이유입니다.

컴포넌트 오버헤드: next/image는 내부적으로 꽤 복잡한 리액트 컴포넌트입니다. 단순 태그나 인라인 는 브라우저가 즉시 그리지만, next/image는 자바스크립트가 실행되고 하이드레이션(Hydration)되는 과정을 거칩니다. 배보다 배꼽이 더 클 수 있죠.

Lazy Loading의 역설: 아주 작은 아이콘은 페이지 로드 시 한꺼번에 가져오는 게 차라리 낫습니다. 굳이 스크롤을 내릴 때마다 하나씩 "나 이제 로드할게!"라고 브라우저에 신호를 보내는 과정 자체가 리소스를 더 먹을 수도 있습니다.

  1. 그럼에도 AI 코드리뷰가 굳이 권고하는 "궁색한 이유"
    AI나 린터(Linter)는 예외 케이스를 고려하지 않고 **'규칙'**만 보기 때문입니다.

규칙의 단순화: "모든 이미지는 next/image를 통한다"라는 규칙을 정해두면, 나중에 누군가 실수로 10MB짜리 SVG(복잡한 지도나 도면 등)를 넣었을 때 사고를 방지할 수 있습니다. 즉, '바보 방지(Fool-proof)' 설계인 셈입니다.

보안 문제 (XSS): SVG 파일 안에 악성 스크립트가 숨겨져 있을 수 있는데, next/image를 통해 처리하면 브라우저가 이를 좀 더 안전하게 다루도록 유도하는 측면이 있습니다. (하지만 이것도 인라인 SVG가 아니면 큰 차이는 없습니다.)

Comment on lines +68 to +78
// 10MB 제한
if (file.size > 10 * 1024 * 1024) {
alert('파일 크기는 10MB를 초과할 수 없습니다.');
return;
}

// 이미지 파일만
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드할 수 있습니다.');
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

파일 크기나 타입 오류를 사용자에게 알리기 위해 alert()를 사용하고 있습니다. alert()는 사용자 경험을 해칠 수 있으므로, 애플리케이션의 다른 부분과 일관된 UI(예: 토스트 메시지, 인라인 에러 메시지)를 사용하는 것이 좋습니다.

Comment on lines +52 to +65
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}. ${month}. ${day}`;
};

const formatLikeCount = (count: number): string => {
if (count >= 1000) {
return '999+';
}
return count.toString();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

formatDateformatLikeCount 함수가 컴포넌트 내부에 정의되어 있어 렌더링될 때마다 새로 생성됩니다. 이 함수들은 순수 함수이므로 컴포넌트 외부로 옮겨 불필요한 재생성을 피하는 것이 좋습니다.


{content && (
<p className={styles.preview}>
{content.length > 100 ? `${content.slice(0, 100)}...` : content}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

본문을 자르는 기준값 100이 하드코딩되어 있습니다. 이 값을 파일 상단에 const CONTENT_PREVIEW_LENGTH = 100;와 같이 상수로 정의하고 사용하면 가독성과 유지보수성이 향상됩니다.

{/* 업로드된 이미지들 */}
{images.map((imageUrl, index) => (
<div key={index} className={`${styles.slot} ${styles[size]}`}>
<img src={imageUrl} alt="" className={styles.preview} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

업로드된 이미지 미리보기의 alt 속성이 비어있습니다. 스크린 리더 사용자를 위해 "업로드된 이미지 1"과 같이 각 이미지를 설명하는 동적인 alt 텍스트를 제공하는 것이 좋습니다.

Suggested change
<img src={imageUrl} alt="" className={styles.preview} />
<img src={imageUrl} alt={`업로드된 이미지 ${index + 1}`} className={styles.preview} />

Comment on lines +42 to +45
.emptySlot {
opacity: 0.3;
cursor: not-allowed;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

.emptySlot 클래스가 정의되어 있지만, ImageUpload.tsx 컴포넌트에서 사용되지 않고 있습니다. 불필요한 코드는 혼란을 줄 수 있으므로 제거하는 것이 좋습니다.

Comment on lines +17 to +36
interface Writer {
id: number;
nickname: string;
image: string | null;
}

/* API 응답 구조 - 댓글 정보 */
interface Comment {
id: number;
content: string;
createdAt: string;
updatedAt: string;
taskId: number;
userId: number;
user: {
id: number;
nickname: string;
image: string | null;
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

WriterComment 인터페이스가 컴포넌트 파일 내에 직접 정의되어 있습니다. 이러한 API 응답 타입들은 애플리케이션의 여러 곳에서 재사용될 가능성이 높으므로, src/types/api.ts와 같은 공통 타입 파일로 분리하여 관리하는 것이 유지보수성 측면에서 더 좋습니다.

}

interface TaskDetailCardProps {
id: number;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

TaskDetailCardProps 인터페이스에 id prop이 정의되어 있지만, 컴포넌트에서 사용되지 않고 있습니다. 만약 id가 필요 없다면 인터페이스에서 제거하여 코드를 더 명확하게 하는 것이 좋습니다.

Comment on lines +61 to +81
function formatDate(dateString: string): string {
const date = new Date(dateString);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? '오후' : '오전';
const displayHours = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;

return `${year}년 ${month}월 ${day}일 ${period} ${displayHours}:${minutes.toString().padStart(2, '0')}`;
}

function formatCommentDate(dateString: string): string {
const date = new Date(dateString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');

return `${year}. ${month}. ${day}`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

날짜를 포맷하는 formatDateformatCommentDate 함수가 컴포넌트 파일 내에 있습니다. 이와 같은 유틸리티 함수들은 src/utils/date.ts와 같은 공통 파일로 분리하여 다른 컴포넌트에서도 재사용할 수 있도록 하는 것이 좋습니다.

@yooolleee yooolleee merged commit c3e829e into main Feb 10, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from 개발 대기중 to 개발 완료 in workers Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

기능추가 기능개발 및 추가할 때 사용해주세요.

Projects

Status: 개발 완료

Development

Successfully merging this pull request may close these issues.

2 participants